var net     = require('net'),
    utility = require('system/utility'),
    dns     = require('dns'),
    os      = require('os'),
    enumerable    = require('collections/enumerable'),
    events  = require('events');

var MAX_INT = 4294967295;
var c       = console;

function log(value) {
	c.log(utility.format('%s %s', new Date().getTime(), value));	
}

var RoutingServer = function(settings) {
	var me = this;
	
	me._sockets        = {};
	me._pendingSockets = {};
	me._localRoutes    = {};
	me._remoteRoutes   = {};
	me._requests       = {};
	me._requestCount   = 0;
	me._serverAddresses = {};
	me._syncIntervalId;
	me._dataBuffer      = '';
	me._dataSeparator   = String.fromCharCode(255);

	me._settings = utility.extend(
		{
			port: 8000,
			callTimeout : 30000,
			connectTimeout : 30000,
			syncInterval: 30000,
			servers : [],
			routes : {}
		},
		settings
	);
}

utility.inherits(RoutingServer, events.EventEmitter);

RoutingServer.prototype.start = function() {
	var me = this;
	
	utility.do(
		function() {
			log('Starting server...');
			
			dns.lookup(os.hostname(), 4, this.parallel());
			dns.lookup(os.hostname(), 6, this.parallel());			
		},
		function(result, onDns4Lookup, onDns6Lookup) {
			
			me._serverAddresses['127.0.0.1:' + me._settings.port]          = { address : '127.0.0.1',       port : me._settings.port };
			me._serverAddresses['0.0.0.0:' + me._settings.port]            = { address : '0.0.0.0',         port : me._settings.port };
			me._serverAddresses[onDns4Lookup[1] + ':' + me._settings.port] = { address : onDns4Lookup[1],   port : me._settings.port };
			me._serverAddresses[onDns6Lookup[1] + ':' + me._settings.port] = { address : onDns6Lookup[1],   port : me._settings.port };
			
			me._settings.routes['/SyncNodes'] = me._onSyncNodes;
			
			// todo: clone routes?			
			me._localRoutes = me._settings.routes;
			
			console.log('localRoutes: ', me._localRoutes);
		},
		function() {			
			_server = net.createServer();
			
			_server.on('connection', function(socket) { me._onConnection(socket); } );
			_server.on('listening', this.parallel());
			_server.on('close', function() { me._onServerClose } );

			_server.listen(me._settings.port);
		},
		function(result, onClient) {
			var worker = this;
		
			if (me._settings.servers) {
				var connectionIndex = 0;
				
				if (onClient) {
					connectionIndex = (onClient[0] == undefined ? -1 : onClient[0]) + 1;
				}
				
				if (connectionIndex < me._settings.servers.length) {
					var address = me._settings.servers[connectionIndex].address;
					var port    = me._settings.servers[connectionIndex].port;
					var done    = worker.parallel();
					
					if (me._serverAddresses[address + ':' + port])
					{
						log(utility.format('Skipping %s:%s', address, port));
						
						process.nextTick(function() {
							worker.repeat();
							done(connectionIndex);
						});
					}
					else
					{
						me._connectToServer(
							address, 
							port,
							function (socket) { // onConnect
								done(connectionIndex);
							},
							function () { // onError
								worker.repeat();
								done(connectionIndex);
							},
							function () { // onTimeout
								worker.repeat();
								done(connectionIndex);
							}
						);
					}				
				}
			}
		},
		function(result) {
			me._syncIntervalId = setInterval(function() { me._onSyncInterval(); }, me._settings.syncInterval);
			me.emit('started');
		}
	);
}
RoutingServer.prototype.call = function(path, args) {
	var me        = this,
	    callback,
	    broadcast = false,
	    route     = me._localRoutes[path],
	    handled   = false;
	
	if (arguments.length > 2) {
		if (arguments[2] === true) {
			broadcast = true;
		} else if (typeof arguments[2] == 'function') {
			callback = arguments[2];
		}
	}
	
	if (arguments.length > 3) {
		if (arguments[3] === true) {
			broadcast = true;
		} else if (typeof arguments[3] == 'function') {
			callback = arguments[3];
		}
	}
	
	args = args.slice();

	if (route) {
		handled = true;
		
		me._callFunction.apply(this, [path, route, args, callback]);
	}

	if (!route || broadcast) {
		route   = me._remoteRoutes[path];

		if (route) {
			handled = true;
log('socket count: ' + utility.inspect(Object.keys(me._sockets)) + ':' + route.sockets.length);			
			if (broadcast) {
				for (var x = 0; x < route.sockets.length; ++x) {
					me._callRemote(path, route.sockets[x], args, callback);
				}
			} else {
				me._callRemote(path, route.sockets[route.index], args, callback);
			}
		}
	}
	
	if (!handled) {
		log(utility.format('Failed to find path %s', path));
		return false;
	}
		
	return true;
}


RoutingServer.prototype._connectToServer = function(address, port, onClientConnect, onClientError, onClientTimeout) {
	var me       = this,
		socket   = new net.Socket(),
		handled  = false,
		timedout = false;
	
log(utility.format('connectToServer called %s:%d', address, port));		
	socket.on('connect', function() {
		if (timedout) {
			socket.end();
		} else {
			handled = true;
			
			delete me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort];
			socket.removeAllListeners();
			
			me._onConnection(socket);
			me._syncNodes(socket);
			
			if (onClientConnect) {
				onClientConnect(socket);
			}
		}
	});
	
	socket.on('error', function() {
		log(utility.format('Failed to connect to %s:%d', address, port));
		
		handled = true;
		
		delete me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort];
		socket.removeAllListeners();
		
		if (!timedout && onClientError) {
			onClientError();
		}
	});
	
	if (onClientTimeout) {
		setTimeout(function() {
			// Do not remove the listeners just in case it eventually connects. 
			if (!handled) {
				log(utility.format('Timed out while connecting to %s:%d', address, port));
				
				timedout = true;
				
				delete me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort];
				onClientTimeout();
			}
		}, me._settings.connectTimeout);
	}

	socket.connectAddress = { address : address, port : port };
	me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort] = socket;
	socket.connect(port, address);
}

RoutingServer.prototype._syncNodes = function(socket) {
	var me = this;
	//console.log('localroutes: ', utility.inspect(me._localRoutes));
	//console.log('linqresult: ', utility.inspect(enumerable.From(me._localRoutes).Select(function(i) { log(i.Key); return i.Key; } ).ToArray()));
			
	me._callRemote(
		'/SyncNodes',
		socket,
		[
			socket.address(),
			_server.address().port, 
			Object.keys(me._localRoutes)
		],
		function (result) {
			if (result) {
				if (result.nodeRoutes) {
					me._syncRoutes(socket, result.nodeRoutes);
				}
				
				if (result.nodes) {
					for (var x = 0; x < result.nodes.length; ++x) {
						if (result.nodes[x] && result.nodes[x].address && !me._sockets[result.nodes[x].address + ':' + result.nodes[x].port]) {
							me._connectToServer(result.nodes[x].address, result.nodes[x].port);
						}
					}
				}
			}
		}
	);
}
	
RoutingServer.prototype._syncRoutes = function(socket, nodeRoutes) {
	var me = this;

	for (var x = 0; x < nodeRoutes.length; ++x) {
		me._addRoute(socket, nodeRoutes[x]);
	}
}	

RoutingServer.prototype._addRoute = function(socket, path) {
	var me    = this,
	    route = me._remoteRoutes[path];

	socket.routes[path] = true;

	if (!route) {
		route = { path: path, index: 0, sockets: [] };
		
		me._remoteRoutes[path] = route;
	}
	
	if (route.sockets.indexOf(socket) < 0) {
		route.sockets.push(socket);
	}
}

RoutingServer.prototype._removeRoutes = function(socket) {
	var me     = this,
	    route;
	
	for (var item in socket.routes) {
		route = me._remoteRoutes[item];
	
		if (route) {
			var x = 0;
		
			while (x < route.sockets.length) {
				if (route.sockets[x] == socket) {
					route.sockets.splice(x, 1);
				} else {
					++x;
				}
			}
			
			if (route.sockets.length == 0) {
				delete me._remoteRoutes[item];
			}
		}
	}
	
	socket.routes = {};
	
	//log(utility.inspect(me._remoteRoutes, true, 1000));	
}

RoutingServer.prototype._callFunction = function(path, fn, args, callback) {
	var me = this;

	log('function call' + path);

	var result = fn.apply(this, args);
	 
	if (callback) {
		process.nextTick(function() {
			callback(result);
		});
	}
}

RoutingServer.prototype._callRemote = function(path, socket, args, callback) {
	var me = this;
	
	console.log('remote call ' + path, ' ', args);
	
	var requestId;

	if (callback) {
		me._addRequestCallback(callback);

		requestId = me._requestCount;
	}

	if (args) {
		for (var x = 0; x < args.length; ++x) {
			if (typeof args[x] == 'function') {
				me._addRequestCallback(args[x]);
				args[x] = { _requestId : me._requestCount };
			}
		}
	}

	me._write(
		socket,
		{
			_path : path,
			_arguments : args,
			_requestId : requestId
			//session : ,
			//callbackReference : ,
		},
		'UTF8'
	);
}

RoutingServer.prototype._addRequestCallback = function(fn) {
	var me        = this,
		requestId = ++me._requestCount;
	
	me._requests[requestId] = fn;
	
	setTimeout(function() {
			delete me._requests[requestId];
		},
		me._settings.callTimeout
	);
	
	if (me._requestCount == MAX_INT) {
		me._requestCount = 0;
	}
}

RoutingServer.prototype._write = function(socket, value, encoding, callback) {
	var me = this;
	
	if (typeof value == 'object') {
		value = JSON.stringify(value);
	}

log(socket.remoteAddress + ':' + socket.remotePort + ' ' + value.length + ' bytes');

//	socket.write(value, encoding, callback);

	if (!socket.writeBuffer) {
		socket.writeBuffer = [];
	}

	if (socket.writeBuffer.length == 0) {
		socket.write(value + me._dataSeparator, encoding, callback);
	}

	socket.writeBuffer.push( { value: value, encoding: encoding, callback: callback } );	
}

RoutingServer.prototype._onData = function(socket, data) {
	var me    = this,
	    json,
	    index = (data = data.toString()).indexOf(me._dataSeparator);

log('onData: ' + Object.keys(me._sockets).length + ' ' + data.toString());
			
	if (index >= 0) {
		index = index + me._dataBuffer.length;
	}
	
	me._dataBuffer = me._dataBuffer + data;
	
	while (index >= 0) {
		data           = me._dataBuffer.substring(0, index);
		me._dataBuffer = me._dataBuffer.substring(index + 1);
		index          = me._dataBuffer.indexOf(me._dataSeparator);

		try {
			json = JSON.parse(data);
		}
		catch (e) {}
		
		if (json) {
			if (json._path && json._arguments) {
			// todo: implement argument functions

				me.call(json._path, json._arguments, function(result) {
					if (json._requestId && !socket.ended) {
						me._write(
							socket,
							{ _responseId: json._requestId, _result : result },
							'UTF8'
						);
					}
				});
				
			} else if (json._responseId) {
				var requestCallback = me._requests[json._responseId];
				
				delete me._requests[json._responseId];
				
				if (requestCallback) {
					requestCallback.apply(socket, [ json._result ]) 
				}
			} else {
				me.emit('jsonData', socket, json);
			}
		} else {
			log('not json');
			//log('');
			//log('');
			//log(data);
			me.emit('data', socket, data);
		}
	}
}

RoutingServer.prototype._onEnd = function(socket) {
	var me = this;
	
	socket.end();
	socket.ended = true;
}

RoutingServer.prototype._onDrain = function(socket) {
	var me = this;

	if (socket.writeBuffer) {
		socket.writeBuffer.shift();
	
		if (socket.writeBuffer.length > 0) {
			socket.write(socket.writeBuffer[0].value + me._dataSeparator, socket.writeBuffer[0].encoding, socket.writeBuffer[0].callback);
		}
	}
}

RoutingServer.prototype._onError = function(socket, err) {
	var me = this;
}

RoutingServer.prototype._onClose = function(socket, address, hadError) {
		var me = this;
		
		if (socket.connectAddress) {
			delete me._sockets[socket.connectAddress.address + ':' + socket.connectAddress.port];
		}
		
		delete me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort];
		
		me._removeRoutes(socket);
		
		log(utility.format('Disconnected from %s:%s', address.address, address.port));
}
	
RoutingServer.prototype._onConnection = function(socket) {
	var me      = this,
	    address = { address: socket.remoteAddress, port: socket.remotePort };
	
	socket.routes = {};

	socket.on('close', function(hadError) { me._onClose(socket, address, hadError); } );
	socket.on('data', function(data) { me._onData(this, data); } );
	socket.on('end', function() { me._onEnd(this); } );
	socket.on('drain', function() { me._onDrain(this); } );

	if (socket.connectAddress) {
		me._sockets[socket.connectAddress.address + ':' + socket.connectAddress.port] = socket;
	} else {
		me._pendingSockets[socket.remoteAddress + ':' + socket.remotePort] = socket;
	}

	me._addRoute(socket, '/SyncNodes');

	if (socket.connectAddress) {
		log(utility.format('Connected to %s:%s',   socket.connectAddress.address, socket.connectAddress.port));
	} else {
		log(utility.format('Connected to %s:%s',   socket.remoteAddress, socket.remotePort));
	}
}

RoutingServer.prototype._onServerClose = function() {
	var me = this;
	
	clearInterval(me._syncIntervalId);
}

RoutingServer.prototype._onSyncInterval = function() {
	var me = this;
	
	for (var item in me._sockets) {
		me._syncNodes(me._sockets[item]);
	}
}
	
RoutingServer.prototype._onSyncNodes = function(remoteAddress, port, nodeRoutes) {
	var me      = this,
	    socket;

	// Recent incoming connection's wont have a connect address so we need to set it and move it from
	// pendingSockets to sockets
	socket = me._pendingSockets[remoteAddress.address + ':' + remoteAddress.port];

	if (socket) {
		log('NO CONNECT ADDRESS');
		
		delete me._pendingSockets[remoteAddress.address + ':' + remoteAddress.port];
		
		// It is possible to have 2 nodes connecting to each other at the same time so we
		// need to check if the socket is already connected.
		if (me._sockets[remoteAddress.address + ':' + port]) {
			log(utility.format('ALREADY CONNECTED TO NODE %s', port));
			socket.end();
			socket.ended = true;
			return;
		} else {
			socket.connectAddress = { address : remoteAddress.address, port : port };
			me._sockets[socket.connectAddress.address + ':' + socket.connectAddress.port] = socket;
		}
	} else {
		socket = me._sockets[remoteAddress.address + ':' + port];
	}

	if (!socket) {
		console.log('Unable to find socket for ', remoteAddress.address, ':', port);
		console.log('pendingsockets', me._pendingSockets);
		console.log('sockets', me._sockets);
		return;
	}

	me._syncRoutes(socket, nodeRoutes);
	
	return {
		nodeRoutes : Object.keys(me._localRoutes), //enumerable.From(me._localRoutes).Select(function(i) { return i.Key; } ).ToArray(),
		nodes : enumerable
					.from(me._sockets)
					.select(function(i) { return i.Value.connectAddress; })
					.where(function(i) { return !(i.address == socket.connectAddress.address && i.port == socket.connectAddress.port); })
					.toArray()
	};
}


module.exports = RoutingServer;
